每天一个技术点

(2023.7.24)软件加密与解密-2-1-程序分析方法[XDbg]

本文作者:XDbgPYG(小吧唧)
发布时间:2023年7月24日
内容概要:练一道题

0. 收集信息

程序名:CrackMeDemo.tvmp.1.exe

程序界面长相如下:

程序内存长相如下:

程序内存字符串长相如下:

看样子是个 MFC 程序(“应用程序向导生成的本地应用程序”、“CWINAPP”)

除此之外有用的信息似乎就是 “恭喜你,注册成功!” 了。

1.反调试

1.1 学习反调试

扔进 OllyDbg 里,F9跑起来。

查看 OllyDbg 日志,并无太多有用信息,只是知道它跑了俩个异常。

  1. 浮点数除以零
  2. 内存访问违规

嗯。。想想有没有办法定位反调试代码范围。

通过例子来学习反调试,点击图片开始学习。

adc

这是我刚才 CV 过来的反调试例子。看样子我们可以试试通过 API 断点,来定位反调试。

if (IsDebuggerPresent())
    ExitProcess(-1);

BOOL bDebuggerPresent;
if (TRUE == CheckRemoteDebuggerPresent(GetCurrentProcess(), &bDebuggerPresent) &&
    TRUE == bDebuggerPresent)
    ExitProcess(-1);

1.2 跟踪反调试

在 CheckRemoteDebuggerPresent 函数下断点,程序断下后,Ctrl + F9 出 CALL。

似乎走到了 ".tvm" 这个代码段来了,简单的使用下我们在日后编写的分析工具 - “基于 CPU 模拟的 xxx ” 。

好的,成功模拟到了 0x040157A 这个地址。通过反汇编代码可以看出,这个代码块似乎含有多个反调试函数,有俩个 jcc 指令跳转到了含有 CALL ExitThread 指令的基本块。

F9 运行到 0x040157A 里,由于我们的反反调试插件强大,所以CheckRemoteDebuggerPresent 返回了 0 ,即程序未被调试的意思。

继续模拟程序,运行到 IsDebuggerPresent 函数中。嗯,是刚才我们学过的反调试内容。

1.3 总结 Call 运行流程

1.3.0 随便看看

将程序扔入 IDA 中 F5 继续分析。根据 IDA 提供的符号,我们大致可以看出这个反调试是在执行 MFC m = new M(b,c) 后才开始检测的。这个信息对于我们完整还原这个程序至关重要,没错,我想把这个程序还原掉~

1.3.1 检测到调试后会执行什么?

让我们看看程序检测调试到后会做些什么?

发现有个 sub_4031f0 call,学过设计模式的朋友应该知道单例模式:即一个类负责一个功能。所以 sub_403530 我是优先不管的。

  if ( cObf_CheckRemoteDebug() == 1 || cObf_IsDebug() == 1 )
  {
    v4 = sub_403530(1);                         // thiscall 不用理他,估计就是销毁 Dialog 用的。
    sub_4031F0(v4);                             // cdeclCall 有点意思~
    ExitThread(8u);
  }

对 sub_4031f0 扫引用,发现有个未被 Ida 识别成函数的地址(0x4021B6)。

转到 0x4021B6 看看,发现有个 call MessageBoxA ,字符串参数为 byte_428D64 。

用 OD 转到 byte_428D64 看看是什么内容。运气不错,直逼关键内容。

让我们对 0x4021B6 附近的代码进行简单的分析。发现一个循环代码,跳转到 loc_401F9A 看看。

发现了一个立即数:8。(如果你足够细心就可以发现,这是我们可输入的用户名的最大长度)

看样子关键算法代码,我们已经定位到了,即在 0x00401FA9 附近。

1.4 拿下第一个符号:MFC_INIT

经过刚才的分析,我们基本可以判断这个 Call 是在做 MFC 初始化 + AntiDebuger 操作了。将其记录下来~ 这是我们逆向出来的第一个符号。这是一个重要的阶段性胜利。

1.5 Patch 掉反调试

这里用最容易被 Anti 掉的过检测方式来过掉反调试。将其 leave ret 即可过掉反调试。(还有内存校验没有解决)

可以看到,这里程序已经正常运行了。hiahia。

2. 逆向算法

OD 转到 0x00401FA9 下查看情况,似乎 IDA 有好多代码没有识别出来,有点小尴尬哈。

找了下函数头部 0x00401B35 ,回到 IDA ,一路选中未识别的代码,按下 C 键完成格式转换。

先静态分析看看函数的控制流程好了,在这里我给出我认为简洁有用的信息。

.text:00401DA6                 call    ?UpdateData@CWnd@@QAEHH@Z ; CWnd::UpdateData(int)
.text:00401DFC                 call    ?GetDlgItem@CWnd@@QBEPAV1@H@Z ; CWnd::GetDlgItem(int)
.text:00401E1F                 call    ?GetDlgItem@CWnd@@QBEPAV1@H@Z ; CWnd::GetDlgItem(int)

.text:00401E9A                 cmp     dword ptr [ebp-8C4h], 8
.text:00401EA1                 jge     short loc_401F21


.text:00402204                 repe cmpsd
.text:00402206                 jnz     short loc_40221C
.text:00402208                 push    0               ; unsigned int
.text:0040220A                 push    0               ; lpCaption
.text:0040220C                 push    offset Text     ; lpText
.text:00402211                 mov     ecx, [ebp-8D0h] ; this
.text:00402217                 call    ?MessageBoxA@CWnd@@QAEHPBD0I@Z ; CWnd::MessageBoxA(char const *,char const *,uint)

用 OD 看看和 MsgBoxA 有关的信息 text:00402204 repe cmpsd。经过测试,发现算法不允许输入中文字符串,尴尬了。

uN: XXXXXDbg
pW: EasyXEasyXEasyXEasyXEasyXEasyXEasyXEasyXEasyX

程序是明码比较,这似乎意味着我们在算法部分可以轻松一下了 O(∩_∩)O。

用 IDA 进行 F5 操作。嗯,好模糊的代码,看样子需要清理花指令才能看清楚。(可是现在已经是 22:11:38 了,一天要结束了,就不写脚本清理了)

数了下大致的代码行数,只有 213 行汇编,直接用 OD 调试还原掉好了。

好了,回到让我们到 0x0401E9A 下断点.

.text:00401E9A                 cmp     dword ptr [ebp-8C4h], 8
.text:00401EA1                 jge     short loc_401F21

经过我努力的跟踪,还原出了如下代码,发现程序获取了代码段的内容并参与到了算法的计算当中,真是个不错的设计。
这样做的话,程序如果直接爆破,生成出来的 serial 必然是错的,难怪程序会直接明码比较,原来暗藏着一股气。

打开 VS ,编写我们的第一段 KeyGen。

// KeyGen.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>

unsigned char t[0x0010] =
{
0xf7, 0xff, 0xff, 0x89, 0x8d, 0x4c, 0xf7, 0xff, 0xff, 0xc7, 0x85, 0x48, 0xf7, 0xff, 0xff, 0x14
};

unsigned char t2[0x0010] =
{
0x07, 0x82, 0x19, 0xe8, 0x1f, 0xfe, 0xff, 0xff, 0x0f, 0xb6, 0xd0, 0x85, 0xd2, 0x74, 0x05, 0xe9
};

void getUn2(unsigned char* _un, unsigned char* _un2)
{
    unsigned char* un = _un;
    unsigned char* un2 = _un2;

    for (size_t i = 0; i < 8; i++)
    {
        int a = (int)*(char*)(t+i);
        int a2 = (int)*(char*)(un + i) ^ i;
        un2[i] = a2 ^ a;
        int b = (int)*(char*)(t2 + i);
        int b2 = (int)*(char*)(un2 + i) ;
        un2[i] =  b2 ^ b;
    }

}

int main()
{
    std::cout << "Hello KeyGen!\n";
    unsigned char uN[] = "XXXXXDbg";
    unsigned char* uN2 = new unsigned char[sizeof(uN)];
    getUn2(uN,uN2);

    return 0;
}


a8 24 bc 3a ce f3 4c 40 // cd fd fd fd
A8 24 BC 3A CE F3 4C 40 // 00 00 00 00

可以看到结果完全正确,让我们继续还原。发现了一个未知的值,[EBP - 0x828],OD 往上翻翻,看看它的算法。

可以看到一个循环很多次的代码,结合刚才的经验,可以看出这个 constV 也是通过代码段的内容算出来的。

这个函数就不用还原了,取消所有软件断点,然后在 0x0401C53 下硬件执行断点,拿到 constV 即可。

constV = 0xCF238EE8;
int constV = 0xCF238EE8;
*(int*)((int*)uN2) ^= constV;
*(int*)((int*)uN2+1) ^= constV;

接下来又是一个循环,估计又有好多表要 CV 下来,哈哈,可以看到依旧循环 8 次。

继续单步~发现了三个 flag 将这段汇编还原成 C 代码~。

还原代码如下。

for (size_t i = 0; i < 8; i++)
    {
        flag1 = ((uN2[i] & 0xE0) +  (((uN2[i] & 0xE0 )>> 24) & 0x1F)) >> 5;
        flag2 = (uN2[i] & 0x1C) + (((uN2[i] & 0x1C) >> 24) & 3) >> 2;
        flag3 = uN2[i] & 0x3;
        
        int flag4 = i / 3;
        int id = i >> 24;
        if (id == 2)
        {
          // .......
        }

        if (id == 1)
        {
          // .......
        }

        if (id == 0)
        {
          // .......
        }
    }

继续单步跟踪,在这块代码中,我们发现几张 table 和新的 flag ,翻翻汇编代码看看 flag 有什么意义。

联系上文可以发现,它存储的就是正确的注册码 Serial。

将 table dump 出来将这个基本块还原成 C 代码。

unsigned char t3_834[0x0040] =
{
0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x00, 0x00, 0x40, 0xed, 0x9f, 0xf5,
0x26, 0x3a, 0x6f, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

unsigned char t4_87c[0x00B0] =
{
0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78,
0x79, 0x7a, 0x6e, 0x69, 0xc9, 0x1b, 0x40, 0x00, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47,
0x48, 0x49, 0x18, 0x00, 0xa0, 0xf0, 0x18, 0x00, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,
0x59, 0x5a, 0x00, 0x00, 0x40, 0xed, 0x9f, 0xf5, 0x26, 0x3a, 0x6f, 0x8f, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

unsigned char t5_864[0x0090] =
{
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x18, 0x00, 0xa0, 0xf0, 0x18, 0x00,
0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50,
0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x00, 0x00, 0x40, 0xed, 0x9f, 0xf5,
0x26, 0x3a, 0x6f, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

        if (id == 0)
        {
            serial[i] = t3_834[flag2];
            serial[i+1] = t4_87c[flag3];
            serial[i+2] = t5_864[flag1];
        }

好了,接下来我们继续还原其他分支,这是 id == 1 的情况。

还原代码如下。

        if (id == 1)
        {
            serial[i] = t3_834[flag1];
            serial[i + 1] = t4_87c[flag2];
            serial[i + 2] = t5_864[flag3];
        }

这是 id == 2 的情况,发现了俩张新的 table:t6、t7 。

将新的 table dump 下来,还原 C 代码如下。

unsigned char t6_83c[0x001E] =
{
0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,
0x59, 0x5A, 0x7F, 0x00, 0xF6, 0x9A, 0xE2, 0xF7, 0x9B, 0x50, 0x09, 0x80, 0x00, 0x00
};

unsigned char t7_874[0x003C] =
{
0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x19, 0x00, 0xC9, 0x1B, 0x40, 0x00,
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F,
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00,
0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C
};


        if (id == 2)
        {
            serial[i] = t5_864[flag3];
            serial[i+ 1] = t6_83c[flag1];
            serial[i + 2] = t7_874[flag2];
        }

在测试的时候,发现 serial 对不上,原因是因为我忘记还原这条汇编指令了、flag4 和 id 的关系搞错了 、constV 提取错了。

IMUL    EAX, EAX, 0x3

//int constV = 0xCF238EE8; 
int constV = 0xE0698E5C;

int flag4 = i % 3;
int id = flag4;

最终输出结果如下。

XXXXXDbg
Vi7Vk21OvWk6Um21LxRj0Ui0

show time

3. tvm?

看样子这个程序被巧妙的绕过了未知的 vm 保护,实际上经过我的测试,这个程序并无 vm 保护,有基于堆栈的花指令混淆。

这在学习第三章也就是下一章代码混淆时,会将其清除~

4. 还原程序

日后再补。

5. 文件下载

CrackMeDemo.tvmp.1.exe

KeyGen